﻿////////////////////////////////////////////////////////////
// Geo.CoordinateConverter.ts                             //
// Michael Kappel, MCSD                                   //
// Geographic coordinate conversion for Geotracakble.mobi //
// 4-22-2013                                              //
//                                                        //
////////////////////////////////////////////////////////////


//////////////CORE//////////////
module Geo.CoordinateConverter {

    export module Enumerations {
        //Step 1 - Geographic coordinate conversion Types//
        //http://en.wikipedia.org/wiki/Geographic_coordinate_conversion//
        export enum DirectionTypes {
             North,
             East,
             South,
             West
        }
        export enum LatitudeLongitudeTypes {
             Latitude,
             Longitude
        }
    }

    export module Interfaces {
        //Step 2 - Geographic coordinate conversion Types//
        export interface IStringable {
            ToString(): string;
        }
        export interface ICoordinateConversions extends IStringable  {
            ToCoordinateDecimal(): ICoordinateDecimal;
            ToCoordinateDms(): ICoordinateDms;
            ToCoordinateDm(): ICoordinateDm;
        }
        export interface ICoordinateDm extends IStringable,ICoordinateConversions {
            DmLatitude: IDm;
            DmLongitude: IDm;
        }
        export interface ICoordinateDms extends IStringable,ICoordinateConversions {
            DmsLatitude: IDms;
            DmsLongitude: IDms;
        }
        export interface ICoordinateDecimal extends IStringable,ICoordinateConversions {
            Latitude: number;
            Longitude: number;
        }
        export interface IDm extends IStringable {
            DirectionType: Enumerations.DirectionTypes;
            Degrees: number;
            Minutes: number;
            
            ToDecimal(): number;
            ToDms(): Interfaces.IDms;
            ToDm(): Interfaces.IDm;
        }
        export interface IDms extends IDm {
            Seconds: number;
        }
        export interface IParseDms {
            ParseCoordinateDms(raw: string): Interfaces.ICoordinateDms;
            TryParseCoordinateDms(raw: string, coordinate: any): bool;
        }
        export interface ICoordinateConverter extends IParseDms {

        }
    }
    //Step 3 - Unit Testing  //

    //Step 4 - Implementation//
    //Data Transfer Object (DTO)//
    export module Model {
        export class CoordinateDecimalInfo implements Interfaces.ICoordinateDecimal {
            public Latitude: number;
            public Longitude: number;
            constructor(decimalLatitude: number, decimalLongitude: number) {
                this.Latitude = decimalLatitude;
                this.Longitude = decimalLongitude;
            }


            public ToString(): string {
                return this.Latitude.toString() + ', ' + this.Longitude.toString();
            }
            public ToCoordinateDecimal(): Interfaces.ICoordinateDecimal {
                return this;
            }
            public ToCoordinateDms(): Interfaces.ICoordinateDms {
                return new CoordinateDmsInfo(
                    this.NumberToDms(Enumerations.LatitudeLongitudeTypes.Latitude, this.Latitude),
                    this.NumberToDms(Enumerations.LatitudeLongitudeTypes.Longitude, this.Longitude));
            }
            public ToCoordinateDm(): Interfaces.ICoordinateDm {
                return new CoordinateDmInfo(
                    this.NumberToDms(Enumerations.LatitudeLongitudeTypes.Latitude, this.Latitude).ToDm(),
                    this.NumberToDms(Enumerations.LatitudeLongitudeTypes.Longitude, this.Longitude).ToDm());
            }

            
            public NumberToDms(latitudeLongitudeType: Enumerations.LatitudeLongitudeTypes, coordinate: number) {
                var result: Interfaces.IDms;

                var degrees: number;
                var minutes: number;
                var seconds: number;

                    //Degrees
                var modDegrees: number = Math.abs(coordinate % 1);
                var degrees: number = Math.round(Math.abs(coordinate)-modDegrees);
                
                //Minutes
                var rawMinutes: number = modDegrees * 60;
                var modMinutes: number = rawMinutes % 1;
                var minutes: number =  Math.round(rawMinutes-modMinutes);
                
                //Seconds
                var seconds: number = parseFloat((modMinutes * 60).toPrecision(6));

                var resultDirectionType: Enumerations.DirectionTypes;
                if (latitudeLongitudeType == Enumerations.LatitudeLongitudeTypes.Latitude) {
                    resultDirectionType = (coordinate > 0) ? Enumerations.DirectionTypes.North : Enumerations.DirectionTypes.South ;
                } else {
                    resultDirectionType = (coordinate > 0) ? Enumerations.DirectionTypes.East : Enumerations.DirectionTypes.West;
                }

                return new Model.DmsInfo(resultDirectionType, Math.abs(degrees), minutes, seconds);
            }
        }
        export class CoordinateDmInfo implements Interfaces.ICoordinateDm {
            private _dmLatitude: Interfaces.IDm;
            private _dmLongitude: Interfaces.IDm;

            constructor(dmLatitude: Interfaces.IDm, dmLongitude: Interfaces.IDm) {
                this._dmLatitude = dmLatitude;
                this._dmLongitude = dmLongitude;
            }

            public get DmLatitude(): Interfaces.IDm {
                return this._dmLatitude;
            }

            public get DmLongitude(): Interfaces.IDm {
                return this._dmLongitude;
            }

            public ToString(): string {
                return this.DmLatitude.ToString() + ', ' + this.DmLongitude.ToString();
            }
            public ToCoordinateDecimal(): Interfaces.ICoordinateDecimal {
                return new CoordinateDecimalInfo(this.DmLatitude.ToDecimal(), this.DmLongitude.ToDecimal());
            }
            public ToCoordinateDms(): Interfaces.ICoordinateDms {
                return new CoordinateDmsInfo(this.DmLatitude.ToDms(), this.DmLongitude.ToDms());
                
            }
            public ToCoordinateDm(): Interfaces.ICoordinateDm {
                return this;
            }
        }
        export class CoordinateDmsInfo implements Interfaces.ICoordinateDms {
            private _dmsLatitude: Interfaces.IDms;
            private _dmsLongitude: Interfaces.IDms;

            constructor(dmsLatitude: Interfaces.IDms, dmsLongitude: Interfaces.IDms) {
                this._dmsLatitude = dmsLatitude;
                this._dmsLongitude = dmsLongitude;
            }

            public get DmsLatitude(): Interfaces.IDms {
                return this._dmsLatitude;
            }

            public get DmsLongitude(): Interfaces.IDms {
                return this._dmsLongitude;
            }

            public ToString(): string {
                return this.DmsLatitude.ToString() + ', ' + this.DmsLongitude.ToString();
            }

            public ToCoordinateDecimal(): Interfaces.ICoordinateDecimal {
                return new CoordinateDecimalInfo(this.DmsLatitude.ToDecimal(), this.DmsLongitude.ToDecimal());
            }
            public ToCoordinateDms(): Interfaces.ICoordinateDms {
                return this;
            }
            public ToCoordinateDm(): Interfaces.ICoordinateDm {
                return new CoordinateDmInfo(this.DmsLatitude.ToDm(), this.DmsLongitude.ToDm());
            }
        }
        
        export class CoordinateBase {
            constructor() { }

            //public ToString(): string {
            //    throw new Error('This method is abstract');
            //}

            GetDirectionAbbreviation(directionType: Enumerations.DirectionTypes): string {
                var result: string;
                if (directionType == Enumerations.DirectionTypes.North) {
                    result = 'N';
                } else if (directionType == Enumerations.DirectionTypes.East) {
                    result = 'E';
                } else if (directionType == Enumerations.DirectionTypes.South) {
                    result = 'S';
                } else if (directionType == Enumerations.DirectionTypes.West) {
                    result = 'W';
                }
                return result;
            }
        }

        export class DmInfo extends CoordinateBase implements Interfaces.IDm {
            private _degrees: number;
            private _minutes: number;
            private _directionType: Enumerations.DirectionTypes;

            constructor(directionType: Enumerations.DirectionTypes, degrees: number, minutes: number)
            {
                super();
                this._directionType = directionType;
                this._degrees = degrees;
                this._minutes = minutes;
            }

            public get DirectionType(): Enumerations.DirectionTypes {
                return this._directionType;
            }

            public get Degrees(): number {
                return this._degrees;
            }

            public get Minutes(): number {
                return this._minutes;
            }

            public ToDecimal(): number {
                var result: number;
                var multiplier: number = (this.DirectionType === Enumerations.DirectionTypes.West ||
                    this.DirectionType === Enumerations.DirectionTypes.South) ? -1 : 1;

                var result: number = multiplier * (
                        this.Degrees
                        + (this.Minutes / 60.0)
                );

                return parseFloat(result.toPrecision(10));
            }
            public ToDm(): Interfaces.IDm {
                return this;
            }
            public ToDms(): Interfaces.IDms {
                var result: Interfaces.IDms;

                var modMinutes: number = this.Minutes % 1;
                var minutesResult: number = Math.round(this.Minutes - modMinutes);
                var secondsResult: number = parseFloat((modMinutes * 60).toPrecision(6));

                return new Model.DmsInfo(this.DirectionType, Math.abs(this.Degrees), minutesResult, secondsResult);
            }

            public ToString(): string {
                return this.GetDirectionAbbreviation(this.DirectionType) + ' ' + this.Degrees + '°' + this.Minutes + '"';
            }

        }
        export class DmsInfo extends CoordinateBase implements Interfaces.IDms {
            private _degrees: number;
            private _minutes: number;
            private _seconds: number;
            private _directionType: Enumerations.DirectionTypes;

            constructor(directionType: Enumerations.DirectionTypes, degrees: number, minutes: number, seconds: number) {
                super();
                this._directionType = directionType;
                this._degrees = degrees;
                this._minutes = minutes;
                this._seconds = seconds;
            }

            get DirectionType(): Enumerations.DirectionTypes {
                return this._directionType;
            }

            get Degrees(): number {
                return this._degrees;
            }

            get Minutes(): number {
                return this._minutes;
            }

            get Seconds(): number {
                return this._seconds;
            }

            public ToDecimal(): number {

                var multiplier: number = (this.DirectionType === Enumerations.DirectionTypes.West ||
                    this.DirectionType === Enumerations.DirectionTypes.South) ? -1 : 1;

                var result: number = multiplier * (
                        this.Degrees
                        + (this.Minutes / 60.0)
                        + (this.Seconds / 3600.0)
                );

                return parseFloat(result.toPrecision(10));
            }
            public ToDm(): Interfaces.IDm {
                var resultMinutes: number = (this.Seconds / 60) + this.Minutes;
                return new Model.DmInfo(this.DirectionType, Math.abs(this.Degrees), resultMinutes);
            }
            public ToDms(): Interfaces.IDms {
                return this;
            }


            public ToString(): string {
                return this.GetDirectionAbbreviation(this.DirectionType) + ' ' + this.Degrees + '°' + this.Minutes + '\'' + this.Seconds + '"';

            }
        }
    }

    //Data Transfer Object (DTO)//
    export module Providers {
        export class DecimalAndDmsConverter implements Interfaces.ICoordinateConverter {
             
            //#region public methods
            public TryParseCoordinateDms(raw: string, coordinate: Interfaces.ICoordinateDms): bool {
                try {
                    coordinate = this.ParseCoordinateDms(raw);
                    return true;
                }catch (ex) {
                    return false;
                }
            }

            public ParseCoordinateDms(raw: string): Interfaces.ICoordinateDms {
                if (raw == null)
                    return null;

                var result: Interfaces.ICoordinateDms;
                var rawStripped: string = raw.replace(/[^nesw0-9-,\.]/ig, ' ').replace(/\s{2,}/ig, ' ');
        
                var latitudePart: Interfaces.IDms;
                var longitudePart: Interfaces.IDms;

                if (rawStripped.indexOf(',') > 0) {
                    if (rawStripped.indexOf(',') !== rawStripped.lastIndexOf(',')){
                        throw 'ParseCoordinateDms Invalid Coordinate Format: multable [,]';
                    }
                    result = this.ToCoordinateDmsFromArray(rawStripped.split(','));
                } else if (rawStripped.indexOf(' ') > 0) {
                    var coordinateParts: string[] = rawStripped.split(' ');
                    var coordinatePart1: string = '';
                    var coordinatePart2: string = '';
                    var maxNumericSegments = Math.floor(coordinateParts.length / 2);
                    var numericSegments = 0;
                    for (var i = 0; i < coordinateParts.length; i++) {
                        if (i < coordinateParts.length / 2 && numericSegments < maxNumericSegments) {
                            coordinatePart1 += coordinateParts[i] + ' ';
                        } else {
                            coordinatePart2 += coordinateParts[i] + ' ';
                        }
                        if (!isNaN(parseFloat(coordinateParts[i])))
                            numericSegments += 1;
                    }
                    result = this.ToCoordinateDmsFromArray([coordinatePart1, coordinatePart2]);
                }
                return result;  
            }
            //#endregion public methods
            //#region private internal methods
            private ParseDms(raw: string): Interfaces.IDms {
                var result: Interfaces.IDms;
                var rawUpper: string = raw.toUpperCase();
                var rawStripped: string = rawUpper.replace(/[^0-9-\.]/ig, ' ').replace(/\s{2,}/ig, ' ').trim();
        
                var directionType: Enumerations.DirectionTypes = this.GetDirectionTypeInput(rawUpper)

                var coordinateParts = rawStripped.split(' ');
                var firstNumber: number = parseFloat(coordinateParts[0]);
                if (coordinateParts.length === 1) {
                    result = this.ToDmsFromCoordinate(directionType, firstNumber);
                } else if (coordinateParts.length === 2) {
                    result = this.ToDmsFromDegreesMinutes(directionType, firstNumber, parseFloat(coordinateParts[1]))
                } else if (coordinateParts.length === 3) {
                    result = this.ToDmsFromDegreesMinutesSeconds(directionType, firstNumber, parseFloat(coordinateParts[1]), parseFloat(coordinateParts[2]))
                } else {
                    throw 'ParseDms Invalid Sections ' + raw;
                }
                return result;  
            }

            private ToCoordinateDmsFromArray(coordinateParts: string[]): Interfaces.ICoordinateDms {
                var latitudePart: Interfaces.IDms;
                var longitudePart: Interfaces.IDms;

                var coordinatePartsUsed: Interfaces.IDms[] = [];
                for (var i = 0; i < coordinateParts.length; i++) {
                        var coordinatePart: string = coordinateParts[i];
                        if (this.HasDirectionType(coordinatePart)) {
                            var directionType: Enumerations.DirectionTypes = this.GetDirectionTypeInput(coordinatePart);
                            if (directionType == Enumerations.DirectionTypes.West || directionType == Enumerations.DirectionTypes.East) {
                                longitudePart = this.ParseDms(coordinatePart);
                                coordinatePartsUsed[i] = longitudePart;
                            } else {
                                latitudePart = this.ParseDms(coordinatePart);
                                coordinatePartsUsed[i] = latitudePart;
                            }
                        }
                    }
                    if (coordinatePartsUsed[0] == null) {                        
                        if (coordinatePartsUsed[1] == null) {
                            latitudePart = this.ParseDms("N" + coordinateParts[0]);
                            longitudePart = this.ParseDms("E" + coordinateParts[1]);
                            coordinatePartsUsed[0] = latitudePart;
                            coordinatePartsUsed[1] = longitudePart;
                        } else if (coordinatePartsUsed[1].DirectionType == Enumerations.DirectionTypes.West || coordinatePartsUsed[1].DirectionType == Enumerations.DirectionTypes.East) {
                            latitudePart = this.ParseDms("N" + coordinateParts[0]);
                            coordinatePartsUsed[0] = latitudePart;
                        } else {
                            longitudePart = this.ParseDms("E" + coordinateParts[0]);
                            coordinatePartsUsed[0] = longitudePart;
                        }
                    }
                    if (coordinatePartsUsed[1] == null) {                    
                        if (coordinatePartsUsed[0].DirectionType == Enumerations.DirectionTypes.West || coordinatePartsUsed[0].DirectionType == Enumerations.DirectionTypes.East) {
                            latitudePart = this.ParseDms("N" + coordinateParts[1]);
                        } else {
                            longitudePart = this.ParseDms("E" + coordinateParts[1]);
                        }
                    }

                var result: Interfaces.ICoordinateDms = new Model.CoordinateDmsInfo(latitudePart,longitudePart);
                return result;
            }

            private ToDmsFromDegreesMinutesSeconds(directionType: Enumerations.DirectionTypes, degrees: number, minutes: number, seconds: number): Interfaces.IDms {
                var result: Interfaces.IDms;
                
                var resultDirectionType: Enumerations.DirectionTypes = this.GetDirectionTypeOutput(directionType, degrees < 0);
                return new Model.DmsInfo(resultDirectionType, Math.abs(degrees), minutes, seconds);
            }
            
            private ToDmsFromDegreesMinutes(directionType: Enumerations.DirectionTypes, degrees: number, minutes: number): Interfaces.IDms {
                var result: Interfaces.IDms;
                
                var resultDirectionType: Enumerations.DirectionTypes = this.GetDirectionTypeOutput(directionType, degrees < 0);
                return (new Model.DmInfo(resultDirectionType, Math.abs(degrees), minutes)).ToDms();
            }

            private ToDmsFromCoordinate(directionType: Enumerations.DirectionTypes, coordinate: number): Interfaces.IDms {
                var result: Interfaces.IDms;

                var degrees: number;
                var minutes: number;
                var seconds: number;

                  //Degrees
                var modDegrees: number = Math.abs(coordinate % 1);
                var degrees: number = Math.round(Math.abs(coordinate)-modDegrees);
                
                //Minutes
                var rawMinutes: number = modDegrees * 60;
                var modMinutes: number = rawMinutes % 1;
                var minutes: number =  Math.round(rawMinutes-modMinutes);
                
                //Seconds
                var seconds: number = parseFloat((modMinutes * 60).toPrecision(6));
                var resultDirectionType: Enumerations.DirectionTypes = directionType;
                
                resultDirectionType = this.GetDirectionTypeOutput(directionType,coordinate < 0);
                return new Model.DmsInfo(resultDirectionType, Math.abs(degrees), minutes, seconds);
            }

            private HasDirectionType(raw: string): bool {
                var rawUpper: string = raw.toUpperCase();
                return (rawUpper.indexOf('N') >= 0 ||
                        rawUpper.indexOf('E') >= 0 ||
                        rawUpper.indexOf('S') >= 0 ||
                        rawUpper.indexOf('W') >= 0);
            }

            private GetDirectionTypeInput(raw: string): Enumerations.DirectionTypes {
                var rawUpper: string = raw.toUpperCase();
                var directionType: Enumerations.DirectionTypes;
                if (rawUpper.indexOf('N') >= 0)
                    directionType = Enumerations.DirectionTypes.North;
                else if (rawUpper.indexOf('E') >= 0)
                    directionType = Enumerations.DirectionTypes.East;
                else if (rawUpper.indexOf('S') >= 0)
                    directionType = Enumerations.DirectionTypes.South;
                else if (rawUpper.indexOf('W') >= 0)
                    directionType = Enumerations.DirectionTypes.West;
                else
                throw 'ParseDms Invalid Coordinate Format requires Direction ' + raw;

                return directionType;
            }

            private GetDirectionTypeOutput(directionType: Enumerations.DirectionTypes, isNegative: bool): Enumerations.DirectionTypes {

                var result: Enumerations.DirectionTypes = directionType;
                if (isNegative && directionType == Enumerations.DirectionTypes.North)
                    result = Enumerations.DirectionTypes.South;
                else if (isNegative && directionType == Enumerations.DirectionTypes.East)
                    result = Enumerations.DirectionTypes.West;
                else if (isNegative && directionType == Enumerations.DirectionTypes.South)
                    result = Enumerations.DirectionTypes.North;
                else if (isNegative && directionType == Enumerations.DirectionTypes.West)
                    result = Enumerations.DirectionTypes.East;

                return result;
            }
            //#endregion private internal methods
        }
    }
    
    export class CoordinateViewModel {
        private _coordinateInputElement: HTMLInputElement;
        private _gpsOutputElement: HTMLHeadingElement;
        private _dmsOutputElement: HTMLHeadingElement;
        private _decimalOutputElement: HTMLHeadingElement;
        private _coordinateConverter: Geo.CoordinateConverter.Interfaces.ICoordinateConverter;
        private _model: Geo.CoordinateConverter.Interfaces.ICoordinateDms;


        constructor(coordinateConverter: Geo.CoordinateConverter.Interfaces.ICoordinateConverter,
                coordinateInputElement: HTMLInputElement,
                gpsOutputElement: HTMLHeadingElement,
                dmsOutputElement: HTMLHeadingElement,
                decimalOutputElement: HTMLHeadingElement,
                onInputChanged: ()=> void
            ) {
            this._coordinateConverter = coordinateConverter;
            this._coordinateInputElement = coordinateInputElement;
            this._gpsOutputElement = gpsOutputElement;
            this._dmsOutputElement = dmsOutputElement;
            this._decimalOutputElement = decimalOutputElement;
            this._coordinateInputElement.onkeyup = onInputChanged;
        }
        public Update(newValue?:string): void {
            try {
                if (newValue) {
                    this._coordinateInputElement.value = newValue;
                } 
                this.Model = this.CoordinateConverter.ParseCoordinateDms(this.CoordinateInput);
                this.CoordinateDecimalOutput = this.Model.ToCoordinateDecimal().ToString();
                this.CoordinateGpsOutput = this.Model.ToCoordinateDm().ToString();
                this.CoordinateDmsOutput = this.Model.ToCoordinateDms().ToString();
            } catch (ex) {
                this.Model = null;
                this.CoordinateDecimalOutput = '';
                this.CoordinateGpsOutput = '';
                this.CoordinateDmsOutput = '';
            }
        }

        public get CoordinateConverter(): Geo.CoordinateConverter.Interfaces.ICoordinateConverter {
            return this._coordinateConverter;
        }
        public get Model(): Geo.CoordinateConverter.Interfaces.ICoordinateDms {
            return this._model;
        }
        public set Model(value:Geo.CoordinateConverter.Interfaces.ICoordinateDms) {
            this._model = value;
        }
        
        public get CoordinateInput(): string {
            return this._coordinateInputElement.value;
        }
        
        //set CoordinateInput(value:string) {
        //    this._coordinateInputElement.value = value;
        //    this._coordinateInputElement.onkeyup(null);
        //}

        public set CoordinateDecimalOutput(value:string) {
            this._decimalOutputElement.innerHTML = value;
        }

        public set CoordinateGpsOutput(value:string) {
             this._gpsOutputElement.innerHTML = value;
        }

        public set CoordinateDmsOutput(value:string) {
             this._dmsOutputElement.innerHTML = value;
        }
    }
}
     